package ngat.oss.simulation;

import ngat.phase2.*;
import ngat.astrometry.*;
import ngat.util.*;
import ngat.util.logging.*;
import ngat.icm.*;

import java.util.*;

/**
 * Provides basic execution timing and feasibility information.
 */
public class BasicExecutionTimingModel implements ExecutionTimingModel, PropertiesConfigurable {

	public static final double DOME_HORIZON = 20.0;

	public static final double DOME_ZENITH = 90.0;

	public static final double MLD = 15.0;

	public static final double NIGHT_ELEV = -18.0; // MSE < NE -> Night (after
													// Astro twilight)

	public static final double DARK_TWILIGHT_ELEV = -12.0; // MSE < DTE ->
															// Astro twilight

	public static final double BRIGHT_TWILIGHT_ELEV = -6.0; // MSE < BTE ->
															// Nautical twilight

	public static final long DEFAULT_MAX_EXEC_TIME = 12 * 3600 * 1000L;

	public static final long FIXED_GROUP_PRE_START_BUFFER = 5 * 60 * 1000L;

	public static final long FIXED_GROUP_POST_START_LAPSE = 10 * 60 * 1000L;

	public static final double DEFAULT_OFFSET_TIME = 10000.0;

	public static final double DEFAULT_READOUT_TIME = 6000.0;

	public static final double DEFAULT_CONFIG_TIME = 5000.0;

	public static final double DEFAULT_SLEW_TIME = 60000.0;

	/** The telescope site. */
	Site site;

	/** The instrument registry. */
	InstrumentRegistry instruments;

	LogProxy logger;

	/** High limit for dome (rads). */
	double domeHighLimit;

	/** Low limit for dome (rads). */
	double domeLowLimit;

	/** Minimum lunar distance to target (rads). */
	double minimumLunarDistance;

	/** Sun elevation for night. */
	double nightElevation;

	/** Sun elevation for dark twilight. */
	double darkTwilightElevation;

	/** Sun elevation for bright twilight. */
	double brightTwilightElevation;

	double offsetTime;

	double readoutTime;

	double configTime;

	double slewTime;

	/** Maximum permitted execution time allowed. */
	long maxExecTime;

	// External time constraint is basically stuff like End_of_Observing,
	// Start_of_Calibration.
	// May be neccessary (esp. for simulation) to keep resetting this or to
	// allow blocks of
	// time to be screened out in advance e.g.
	// addExternalTimeConstraintBlock(t1,t2, desc)

	/** An externally imposed time constraint. */
	long externalTimeConstraint;

	/** A description of the externally imposed time constraint. */
	String externalTimeConstraintDescription;

	/**
	 * Buffer time before a fixed-time group is due do we want to exclude other
	 * groups from executing in.
	 */
	long fixedGroupPreStartBufferTime;

	/**
	 * How long after a fixed-time group should have started can we still allow
	 * it to start.
	 */
	long fixedGroupPostStartLapseTime;

	/** A list of unavailable windows (e.g. RTI). */
	List unavailableWindows;

	/**
	 * A list of groups which are to be temporarily vetoed until a specified
	 * time.
	 */
	Map tabuGroups;

	/** A list of known fixed groups. */
	List fixedGroups;

	public BasicExecutionTimingModel(Site site, InstrumentRegistry instruments) {
		this.site = site;
		this.instruments = instruments;

		Logger slogger = LogManager.getLogger("SIM");
		logger = new LogProxy("BXTM", "", slogger);

		tabuGroups = new HashMap();
		fixedGroups = new Vector();
	}

	/**
	 * Load various configurable settings from a config.
	 * 
	 * @see ngat.util.PropertiesConfigurable.
	 */
	public void configure(ConfigurationProperties config) throws Exception {
		domeLowLimit = Math.toRadians(config.getDoubleValue("dome.horizon",
				DOME_HORIZON));
		domeHighLimit = Math.toRadians(config.getDoubleValue("dome.zenith",
				DOME_ZENITH));

		minimumLunarDistance = Math
				.toRadians(config.getDoubleValue("mld", MLD));

		nightElevation = Math.toRadians(config.getDoubleValue(
				"night.elevation", NIGHT_ELEV));
		darkTwilightElevation = Math.toRadians(config.getDoubleValue(
				"dark.twilight.elevation", DARK_TWILIGHT_ELEV));
		brightTwilightElevation = Math.toRadians(config.getDoubleValue(
				"bright.twilight.elevation", BRIGHT_TWILIGHT_ELEV));
		fixedGroupPreStartBufferTime = config.getLongValue(
				"fixed.group.pre.start.buffer", FIXED_GROUP_PRE_START_BUFFER);
		fixedGroupPostStartLapseTime = config.getLongValue(
				"fixed.group.post.start.lapse", FIXED_GROUP_POST_START_LAPSE);

		maxExecTime = config.getLongValue("max.exec.time",
				DEFAULT_MAX_EXEC_TIME);

		offsetTime = config.getDoubleValue("offset.time", DEFAULT_OFFSET_TIME);
		readoutTime = config.getDoubleValue("readout.time",
				DEFAULT_READOUT_TIME);
		configTime = config.getDoubleValue("config.time", DEFAULT_CONFIG_TIME);
		slewTime = config.getDoubleValue("slew.time", DEFAULT_SLEW_TIME);

	}

	/** Add the list of known unavailable (probably RTI) windows. */
	public void setUnavailableWindows(List uaWindows) {
		this.unavailableWindows = uaWindows;
	}

	/** Clear the Tabu list. */
	public void clearTabuGroups() {
		tabuGroups.clear();
	}

	/** Add the specified group to the Tabu list until specified time. */
	public void addTabuGroup(Group group, long until) {
		tabuGroups.put(group, new Long(until));
	}

	/** Clear the FixedGroup list. */
	public void clearFixedGroups() {
		fixedGroups.clear();
	}

	/** Add the specified group to the list of fixed groups. */
	public void addFixedGroup(FixedGroup fg) {
		fixedGroups.add(fg);
	}

	/**
	 * Set an arbitrary external time constraint - obs must be done by this
	 * time.
	 */
	public void setExternalTimeConstraint(long etc, String description) {
		this.externalTimeConstraint = etc;
		this.externalTimeConstraintDescription = description;
	}

	/**
	 * Set the buffer time before a fixed-time group is due do we want to
	 * exclude other groups from executing in.
	 */
	public void setFixedGroupPreStartBufferTime(long t) {
		this.fixedGroupPreStartBufferTime = t;
	}

	/**
	 * Set how long after a fixed-time group should have started can we still
	 * allow it to start.
	 */
	public void setFixedGroupPostStartLapseTime(long t) {
		this.fixedGroupPostStartLapseTime = t;
	}

	// some other stuff we may need to either pass in or setup before calling
	// this fn.

	// full skymodel for current photom, skyb - we can use the passed in Env
	// snapshot for now...
	// fixed-group list for nfg time - DONE but needs pulling out by sim
	// controller first.
	// next external time constraint - DONE needs setting by sim controller
	// first.
	// time of sunrise - pre-calculated - No we work it out each call.
	// TODO instrument registry and all controllers/monitors/capabilityProviders
	//

	// public boolean canDo(Group group, long time, int seeing, long
	// lastExecution, int countExecutions) {

	/**
	 * Returns true if the group can be done at time given current seeing and
	 * time last executed at.
	 * 
	 * @param group
	 *            The group we are testing.
	 * @param time
	 *            When we want to test the group.
	 * @param env
	 *            A snapshot of the observing environment at time.
	 * @param h
	 *            A snapshot summary of the group's execution history at time.
	 */
	public boolean canDo(Group group, 
			     long time, 
			     EnvironmentSnapshot env,
			     ExecutionStatistics h) {
	    logger.log(3, "Group " + group.getName()+" cando at: "+ScheduleSimulator.sdf.format(new Date(time)));

	    long lastExecution = h.lastExecuted;
	    int countExecutions = h.countExecutions;

		int seeing = env.seeing;
		boolean photometric = env.photom;

//		// START MARKOUT
//
		// when will it finish.
		long exec = getExecTime(group);
		if (! isFeasible(group, time))
			return false;
//
//		// VETO: Exec time too long
//		if (exec > maxExecTime) {
//			logger.log(3, "Group " + group.getName()
//					+ " failed: EXEC_TIME_TOO_LONG");
//			return false;
//		}
//
//		// VETO: Sun is still up.
//		Position sun = Astrometry.getSolarPosition(time);
//
//		double sunElev = sun.getAltitude(time, site);
//		boolean sunup = (sunElev > Math.toRadians(-6.0));
//
//		double sunElevLater = sun.getAltitude(time + exec, site);
//		boolean sunWillBeUp = (sunElevLater > Math.toRadians(-6.0));
//
//		if (sunup) {
//			logger.log(3, "Group " + group.getName() + " failed: SUNUP");
//			return false;
//		}
//
//		// VETO: Group - has expired.
//		if (group.getExpiryDate() < time) {
//			logger.log(3, "Group " + group.getName() + " failed: EXPIRED");
//			return false;
//		}
//		// END MARKOUT 1

		// VETO: Group - is locked for edit.
		if (group.isLocked()) {
			logger.log(3, "Group " + group.getName() + " failed: LOCKED");
			return false;
		}

		// VETO: Group - execution overlaps RTI window.
		if (overlapsUnavailableWindow(time, time + exec)) {
			logger.log(3, "Group " + group.getName()
					+ " failed: RTI_WINDOW_OVERLAP");
			return false;
		}

//		// START MARKOUT 2
//		// VETO: Group - cannot complete before sunrise.
//		if (sunWillBeUp) {
//			logger.log(3, "Group " + group.getName()
//					+ " failed: SUN_WILL_RISE_B4_DONE in < " + exec + "S");
//			return false;
//		}
//		// END MARKOUT 2
		// VETO: Group - cannot complete before an arbitrary external time
		// constraint.
		if ((time + exec) > externalTimeConstraint) {
			logger.log(3, "Group "
					+ group.getName()
					+ " failed: EXT_TIME_CONST ("
					+ externalTimeConstraintDescription
					+ ") in "
					+ ((externalTimeConstraint - time) / 1000)
					+ "S at "
					+ ScheduleSimulator.sdf.format(new Date(
							externalTimeConstraint)));
			return false;
		}

		// VETO: Group - cannot complete before next available fixed group (if
		// any) is due.
		// watch this we need to skip past old FGs !

		if (hasMoreFixedGroups(time)) {
			FixedGroup nextFixedGroup = getNextFixedGroup(time);
			long nextFixedGroupTime = nextFixedGroup.getFixedTime();

			if ((time < nextFixedGroupTime + fixedGroupPostStartLapseTime)
					&& (time + exec) > nextFixedGroupTime
							- fixedGroupPreStartBufferTime) {
				logger.log(2, "Group " + group.getName()
						+ " failed: FIXED_GROUP_DUE ("
						+ nextFixedGroup.getName() + ") in "
						+ ((nextFixedGroupTime - time) / 1000) + "S, Require: "
						+ (exec / 1000) + "S");
				return false;
			}

		}

		// VETO: Group - is on Tabu list
		if (isTabu(group, time)) {
			logger.log(3, "Group " + group.getName() + " failed: TABU_LIST");
			return false;
		}

		// VETO: Group - various timing constraints.
		if (group instanceof MonitorGroup) {
			MonitorGroup mg = (MonitorGroup) group;
			long startDate = mg.getStartDate();
			long period = mg.getPeriod();
			float floatFraction = mg.getFloatFraction();

			double fPeriod = (double) (time - startDate) / (double) period;
			double iPeriod = Math.rint(fPeriod);

			if (startDate > time) {
				logger.log(3, "Group " + group.getName()
						+ " failed: MG_PRE_START");
				return false;
			}

			long endDate = mg.getEndDate();
			if (endDate <= time) {
				logger.log(3, "Group " + group.getName()
						+ " failed: MG_POST_END");
				return false;
			}

			long startFloat = startDate
					+ (long) ((iPeriod - (double) floatFraction / 2.0) * (double) period);
			long endFloat = startDate
					+ (long) ((iPeriod + (double) floatFraction / 2.0) * (double) period);

			if (!((startFloat <= time) && (endFloat >= time))) {
				logger.log(3, "Group " + group.getName()
						+ " failed: MG_OUT_WINDOW");
				return false;
			}

			// check wasnt done this window
			long lastDone = lastExecution;
			if (lastDone >= startFloat && lastDone <= endFloat) {
				logger.log(3, "Group " + group.getName()
						+ " failed: MG_EXEC_IN_WINDOW");
				return false;
			}

		} else if (group instanceof EphemerisGroup) {

			EphemerisGroup eg = (EphemerisGroup) group;

			long startDate = eg.getStart();
			long endDate = eg.getEnd();
			long period = eg.getPeriod();
			double phase = (double) eg.getPhase();
			double slopFraction = (double) eg.getSlopPhase();

			double fperiod = Math.floor((time - startDate) / period);
			int iperiod = (int) fperiod;

			if (startDate > time) {
				logger.log(3, "Group " + group.getName()
						+ " failed: EG_PRE_START");
				return false;
			}

			if (endDate <= time) {
				logger.log(3, "Group " + group.getName()
						+ " failed: EG_POST_END");
				return false;
			}

			long startWindow = startDate
					+ (long) ((fperiod + phase - slopFraction / 2.0) * period);
			long endWindow = startDate
					+ (long) ((fperiod + phase + slopFraction / 2.0) * period);

			if (!((startWindow <= time) && (endWindow >= time))) {
				logger.log(3, "Group " + group.getName()
						+ " failed: EG_OUTSIDE_SLOP");
				return false;
			}

			// fail if it has been done by time.
			long lastDone = lastExecution;
			if (lastDone < time) {
			    logger.log(3, "Group " + group.getName()+ " failed: EG_EXECED");
			    return false;
			}

		} else if (group instanceof FixedGroup) {

			// fail if it has been done by time.
			long lastDone = lastExecution;
			if (lastDone < time) {
			    logger.log(3, "Group " + group.getName()+ " failed: FG_EXECED");
			    return false;
			}

			FixedGroup fg = (FixedGroup) group;

			// FG can be done within specified minutes of the fixed time.
			long fixed = fg.getFixedTime();
			if ((fixed < (time - fixedGroupPreStartBufferTime))
					|| (fixed > (time + fixedGroupPostStartLapseTime))) {
			    logger.log(3, "Group " + group.getName()+ " failed: FG_OUT_SLOP");
			    return false;
			}
			
		} else if (group instanceof RepeatableGroup) {
		    
			RepeatableGroup rg = (RepeatableGroup) group;

			long startDate = rg.getStartDate();
			long endDate = rg.getEndDate();
			long minInterval = rg.getMinimumInterval();
			int maxRepeats = rg.getMaximumRepeats();

			if (startDate > time) {
			    logger.log(3, "Group " + group.getName()+ " failed: RG_PRE_START");
				return false;
			}

			if (endDate < time) {
			    logger.log(3, "Group " + group.getName()+ " failed: RG_PAST_END");
			    return false;
			}

			if ((time - lastExecution) <= minInterval) {
			    logger.log(3, "Group " + group.getName()+ " failed: REPEATABLE_TOO_SOON_AFTER_LAST_EXEC");
				return false;
			}

			if (maxRepeats < countExecutions) {
			    logger.log(3, "Group " + group.getName()+ " failed: REPEATABLE_TOO_MANY_REPEATS");
				return false;
			}

		} else {
		    
		    if (group.getStartingDate() > time) {
				logger.log(3, "Group " + group.getName()+ " failed: FLEX_PRE_START");
				return false;
			}

			if (group.getExpiryDate() < time) {
			    logger.log(3, "Group " + group.getName()+ " failed: FLEX_POST_END");
				return false;
			}

			// we can always do a flexible group - thats why they are called
			// flexible !
			// unless it was already done by time.
			long lastDone = lastExecution;
			if (lastDone > 0 && lastDone <= time) {
			    logger.log(3, "Group " + group.getName()+ " failed: FLEX_DONE");
			    return false;
			}
		}

		// check the other constraints.

		// VETO: Group - seeing constraint.
		if (seeing < group.getMinimumSeeing()) {
			logger.log(3, "Group " + group.getName() + 
				   " failed: SEEING_BAD: Group requires: "
				   + Group.toSeeingString(group.getMinimumSeeing())
				   + " Actual: " + Group.toSeeingString(seeing));
			return false;
		}
		
		// VETO: Group - photometricity constraint.
		if (group.getPhotometric() && !photometric) {
		    logger.log(3, "Group " + group.getName()+ " failed:  NON_PHOTOMETRIC");
		    return false;
		}
//		// START MARKOUT 3
//		// VETO: Group - lunar (moon up/down) constraint.
//		Position moon = Astrometry.getLunarPosition(time);
//		boolean moonup = moon.getAltitude(time, site) > Math.toRadians(-2.0);
//		if (group.getMinimumLunar() == Group.DARK && moonup) {
//			// System.err.println("Moon up fail: "+group.getFullPath());
//			logger.log(3, "Group " + group.getName() + " failed: DARK_MOON_UP");
//			return false;
//		}
//
//		// VETO: Group - twilight /sky darkness constraint.
//		boolean allowsky = false;
//		int sky = Group.SKY_ANY;
//		if (sunElev < nightElevation)
//			sky = Group.SKY_NIGHT;
//		else if (sunElev < darkTwilightElevation)
//			sky = Group.SKY_DARK_TWILIGHT;
//		else if (sunElev < brightTwilightElevation)
//			sky = Group.SKY_BRIGHT_TWILIGHT;
//		else
//			sky = Group.SKY_ANY;
//
//		switch (group.getTwilightUsageMode()) {
//		case Group.SKY_ANY:
//			// we allow whatever the sun elev.
//			allowsky = true;
//			break;
//		case Group.SKY_BRIGHT_TWILIGHT:
//			// allow only if sun elev below brightTwilightAngle
//			allowsky = (sunElev < brightTwilightElevation);
//			break;
//		case Group.SKY_DARK_TWILIGHT:
//			// allow only if sun elev below darkTwilightAngle
//			allowsky = (sunElev < darkTwilightElevation);
//			break;
//		case Group.SKY_NIGHT:
//			// allow only if sun elev below nightAngle
//			allowsky = (sunElev < nightElevation);
//		}
//		if (!allowsky) {
//			logger.log(3, "Group " + group.getName() + " failed: TWILIGHT:"
//					+ " Group requires: "
//					+ Group.toTwilightString(group.getTwilightUsageMode())
//					+ " Actual: " + Group.toTwilightString(sky));
//
//			return false;
//		}
//		// END MARKOUT 3

//		// START MARKOUT 4
//		// Observation level vetos...
//		int nobs = 0;
//		Iterator iobs = group.listAllObservations();
//		while (iobs.hasNext()) {
//			Observation obs = (Observation) iobs.next();
//
//			Source src = obs.getSource();
//
//			// VETO: Obs - No target specified.
//			if (src == null) {
//				logger
//						.log(3, "Group " + group.getName()
//								+ " failed: NO_TARGET");
//				return false;
//			}
//
//			// VETO: Obs - Non-extra-solar target - because these are a pain in
//			// the arse.
//			// snf 14-nov-07 allowing for a bit to see what happens
//			// if (! (src instanceof ExtraSolarSource)) {
//			// logger.log(3, "Group "+group.getName()+" failed:
//			// US_TARGET_CLASS");
//			// return false;
//			// }
//
//			Position target = src.getPosition();
//
//			double elev = target.getAltitude(time, site);
//
//			// VETO: Obs - target below dome lower limit.
//			if (elev < domeLowLimit) {
//				logger.log(3, "Group " + group.getName()
//						+ " failed: TARGET_LOW");
//				return false;
//			}
//
//			// VETO: Obs - target above dome upper limit.
//			if (elev > domeHighLimit) {
//				logger.log(3, "Group " + group.getName()
//						+ " failed: TARGET_HIGH");
//				return false;
//			}
//
//			double elevLater = target.getAltitude(time + exec, site);
//			// TODO VETO: Obs - Target will set before exec.
//			if (elevLater < domeLowLimit) {
//				logger.log(3, "Group " + group.getName()
//						+ " failed: TARGET_WILL_SET");
//				return false;
//			}
//
//			// TODO VETO: Obs - Check temporary altitude ceiling (engineering).
//			// TODO VETO: Obs - Check zenith crossing thro ZEZ.
//
//			// TODO VETO: Obs - HA Limit constraint (Merdian or HA or Airmass
//			// constraint).
//			// TODO VETO: Obs - Axis limits - will cross limits (engineering).
//			// TODO VETO: Obs - Axis (rotator) override mode (CP/engineering).
//
//			// TODO VETO: Obs - Autoguider availability.
//			// if (! tcm.getAutoguider().isOnline() &&
//			// obs.getAutoGuiderUsageMode() == TelescopeConfig.AGMODE_MANDATOR)
//			// {
//			// logger.log(3, "Group "+group.getName()+" failed:
//			// MANDATORY_AG_OFFLINE");
//			// return false;
//			// }
//
//			// check all targets are far enough from moon
//			// - this looks to work with diff sites as independant of alt/az.
//
//			// VETO: Obs - target close to moon.
//			double lunarDistance = target.getAngularDistance(moon);
//
//			if (group.getMinimumLunar() == Group.BRIGHT && moonup) {
//				double mld = group.getMinimumLunarDistance();
//				if (mld <= 0.0) {
//					// None specified - use default
//					if (lunarDistance < minimumLunarDistance) {
//						logger.log(3, "Group " + group.getName()
//								+ " failed: BRIGHT_MOON_DEF_CLOSE def_mld= "
//								+ minimumLunarDistance + " ld= "
//								+ lunarDistance);
//						return false;
//					}
//				} else {
//					// Group has specific MLD.
//					if (lunarDistance < mld) {
//						logger.log(3, "Group " + group.getName()
//								+ " failed: BRIGHT_MOON_CLOSE spec_mld= " + mld
//								+ " ld= " + lunarDistance);
//						return false;
//					}
//				}
//
//			}
//			// END MARKOUT 4
			// // Instrumentation.
			// try {
			// InstrumentConfig cfg = obs.getInstrumentConfig();

			// // VETO: Obs - No instrument config specified.
			// if (cfg == null) {
			// logger.log(3, "Group "+group.getName()+" failed:
			// NO_INST_CONFIG");
			// return false;
			// }

			// // VETO: Obs - Unknown instrument (for specified config).
			// InstrumentController icm =
			// instruments.getControllerForConfig(cfg);
			// if (icm == null) {
			// logger.log(3, "Group "+group.getName()+" failed: UNKNOWN_INST no
			// mapping for: "+cfg.getClass().getName());
			// return false;
			// }

			// // VETO: Obs - invalid instrument config.
			// if (! icm.isConfigurable(cfg)) {
			// logger.log(3, "Group "+group.getName()+" failed: INVALID_CONFIG:
			// "+icm.getInstrumentName()+" not feasible: "+cfg);
			// return false;
			// }

			// // VETO: Obs - Instrument is online [icm.isOnline()].
			// if (! icm.isOnline()) {
			// logger.log(3, "Group "+group.getName()+" failed:
			// "+icm.getInstrumentName()+"_OFFLINE");
			// return false;
			// }

			// // VETO: Obs - Instrument is operational [icm.isImpaired()
			// icm.getOperatingEfficiency()].
			// if (icm.isImpaired()) {
			// logger.log(3, "Group "+group.getName()+" failed: ICS_IMPAIRED");
			// return false;
			// }
			// } catch (Exception e) {
			// logger.log(3, "Group "+group.getName()+" failed:
			// INST_REG_OFFLINE");
			// return false;
			// }
//
//			nobs++;
//		}

//		// VETO: No obs in group.
//		if (nobs == 0) {
//			logger.log(3, "Group " + group.getName()
//					+ " failed: NO_OBS_IN_GROUP");
//			return false;
//		}

		return true;

	}

	
	/**
	 * Returns the execution time of the group. New improved method based on
	 * standard timings for each operation.
	 */
	public long getExecTime(Group group) {

		int countObs = 0; // counts obs.
		int countSlews = 1; // counts distinct source moves - include initial
							// slew onto (first) target
		int countOffsets = 0; // counts mosaic offsets
		int countExposures = 0; // counts exposures
		int countConfigs = 0; // counts config changes

		double sumExpose = 0.0;

		Observation observation = null;
		Source source = null;
		Source prevSource = null;
		Position target = null;
		Position prevTarget = null;
		InstrumentConfig instConfig = null;
		InstrumentConfig prevConfig = null;

		// check for sequencing here and create it if it doesnt exist...
		Vector v = null;
		v = group.getSequence();

		if (v == null || v.size() == 0) {
			v = new Vector();
			Iterator it = group.listAllObservations();
			while (it.hasNext()) {
				observation = (Observation) it.next();
				v.add(observation.getName());
			}
		}

		int nseq = v.size();

		Iterator is = v.iterator();
		while (is.hasNext()) {
			String obsName = (String) is.next();
			observation = group.findObservation(obsName);

			countObs++;

			instConfig = observation.getInstrumentConfig();

			source = observation.getSource();
			target = source.getPosition();

			// If this is NOT first obs. There IS a previous source.
			if (countObs != 1) {
				if (source != prevSource) {
					// New source. Add Slew time
					countSlews++;
				}

				if (!(instConfig.sameAs(prevConfig))) {
					// New config. Add cfg time
					countConfigs++;
				}
			}
			prevSource = source;
			prevTarget = target;
			prevConfig = instConfig;

			// Add up all the contributions to this Obo's time.

			// Mosaics.
			int mosaic = 1;
			switch (observation.getMosaic().getPattern()) {
			case Mosaic.ARRAY:
				mosaic = observation.getMosaic().getCellsRA()
						* observation.getMosaic().getCellsDec();
				break;
			case Mosaic.SINGLE:
				mosaic = 1;
				break;
			case Mosaic.VERTICAL:
			case Mosaic.HORIZONTAL:
			case Mosaic.SLOPE_UP:
			case Mosaic.SLOPE_DOWN:
				mosaic = 3;
				break;
			case Mosaic.CROSS:
			case Mosaic.X:
				mosaic = 5;
				break;
			case Mosaic.HOLE:
				mosaic = 8;
				break;
			case Mosaic.GRID:
				mosaic = 9;
				break;
			default:
				mosaic = 1;
			}

			// dont count the first one

			// Multruns.
			int multruns = observation.getNumRuns();

			countOffsets += (mosaic - 1);
			countExposures += multruns * mosaic;

			sumExpose += mosaic * multruns * (observation.getExposeTime()); // convert
																			// msec
																			// to
																			// sec

		} // per observation.

		double totalExec = countOffsets * offsetTime + countExposures
				* readoutTime + sumExpose + countConfigs * configTime
				+ countSlews * slewTime;

		return (long) totalExec;

	}

	public boolean isFeasible(Group group, long time) {
		// when will it finish.
		long exec = getExecTime(group);

		// VETO: Exec time too long
		if (exec > maxExecTime) {
			logger.log(3, "Group " + group.getName()
					+ " failed: EXEC_TIME_TOO_LONG at "+(exec/1000)+"S");
			return false;
		}

		// VETO: Sun is still up.
		Position sun = Astrometry.getSolarPosition(time);

		double sunElev = sun.getAltitude(time, site);
		boolean sunup = (sunElev > Math.toRadians(-6.0));

		double sunElevLater = sun.getAltitude(time + exec, site);
		boolean sunWillBeUp = (sunElevLater > Math.toRadians(-6.0));

		if (sunup) {
			logger.log(3, "Group " + group.getName() + " failed: SUNUP");
			return false;
		}
		if (sunWillBeUp) {
			logger.log(3, "Group " + group.getName()
					+ " failed: SUN_WILL_RISE_B4_DONE in < " + exec + "S");
			return false;
		}
		// VETO: Group - has expired.
		if (group.getExpiryDate() < time) {
			logger.log(3, "Group " + group.getName() + " failed: EXPIRED");
			return false;
		}
		// VETO: Group - lunar (moon up/down) constraint.
		Position moon = Astrometry.getLunarPosition(time);
		boolean moonup = moon.getAltitude(time, site) > Math.toRadians(-2.0);
		if (group.getMinimumLunar() == Group.DARK && moonup) {
			// System.err.println("Moon up fail: "+group.getFullPath());
			logger.log(3, "Group " + group.getName() + " failed: DARK_MOON_UP");
			return false;
		}

		// VETO: Group - twilight /sky darkness constraint.
		boolean allowsky = false;
		int sky = Group.SKY_ANY;
		if (sunElev < nightElevation)
			sky = Group.SKY_NIGHT;
		else if (sunElev < darkTwilightElevation)
			sky = Group.SKY_DARK_TWILIGHT;
		else if (sunElev < brightTwilightElevation)
			sky = Group.SKY_BRIGHT_TWILIGHT;
		else
			sky = Group.SKY_ANY;

		switch (group.getTwilightUsageMode()) {
		case Group.SKY_ANY:
			// we allow whatever the sun elev.
			allowsky = true;
			break;
		case Group.SKY_BRIGHT_TWILIGHT:
			// allow only if sun elev below brightTwilightAngle
			allowsky = (sunElev < brightTwilightElevation);
			break;
		case Group.SKY_DARK_TWILIGHT:
			// allow only if sun elev below darkTwilightAngle
			allowsky = (sunElev < darkTwilightElevation);
			break;
		case Group.SKY_NIGHT:
			// allow only if sun elev below nightAngle
			allowsky = (sunElev < nightElevation);
		}
		if (!allowsky) {
			logger.log(3, "Group " + group.getName() + " failed: TWILIGHT:"
					+ " Group requires: "
					+ Group.toTwilightString(group.getTwilightUsageMode())
					+ " Actual: " + Group.toTwilightString(sky));

			return false;
		}

		// Observation level vetos...
		int nobs = 0;
		Iterator iobs = group.listAllObservations();
		while (iobs.hasNext()) {
			Observation obs = (Observation) iobs.next();

			Source src = obs.getSource();

			// VETO: Obs - No target specified.
			if (src == null) {
				logger
						.log(3, "Group " + group.getName()
								+ " failed: NO_TARGET");
				return false;
			}

			// VETO: Obs - Non-extra-solar target - because these are a pain in
			// the arse.
			// snf 14-nov-07 allowing for a bit to see what happens
			// if (! (src instanceof ExtraSolarSource)) {
			// logger.log(3, "Group "+group.getName()+" failed:
			// US_TARGET_CLASS");
			// return false;
			// }

			Position target = src.getPosition();

			double elev = target.getAltitude(time, site);

			// VETO: Obs - target below dome lower limit.
			if (elev < domeLowLimit) {
				logger.log(3, "Group " + group.getName()
						+ " failed: TARGET_LOW");
				return false;
			}

			// VETO: Obs - target above dome upper limit.
			if (elev > domeHighLimit) {
				logger.log(3, "Group " + group.getName()
						+ " failed: TARGET_HIGH");
				return false;
			}

			double elevLater = target.getAltitude(time + exec, site);
			// TODO VETO: Obs - Target will set before exec.
			if (elevLater < domeLowLimit) {
				logger.log(3, "Group " + group.getName()
						+ " failed: TARGET_WILL_SET");
				return false;
			}

			// TODO VETO: Obs - Check temporary altitude ceiling (engineering).
			// TODO VETO: Obs - Check zenith crossing thro ZEZ.

			// TODO VETO: Obs - HA Limit constraint (Merdian or HA or Airmass
			// constraint).
			// TODO VETO: Obs - Axis limits - will cross limits (engineering).
			// TODO VETO: Obs - Axis (rotator) override mode (CP/engineering).

			// TODO VETO: Obs - Autoguider availability.
			// if (! tcm.getAutoguider().isOnline() &&
			// obs.getAutoGuiderUsageMode() == TelescopeConfig.AGMODE_MANDATOR)
			// {
			// logger.log(3, "Group "+group.getName()+" failed:
			// MANDATORY_AG_OFFLINE");
			// return false;
			// }

			// check all targets are far enough from moon
			// - this looks to work with diff sites as independant of alt/az.

			// VETO: Obs - target close to moon.
			double lunarDistance = target.getAngularDistance(moon);

			if (group.getMinimumLunar() == Group.BRIGHT && moonup) {
				double mld = group.getMinimumLunarDistance();
				if (mld <= 0.0) {
					// None specified - use default
					if (lunarDistance < minimumLunarDistance) {
						logger.log(3, "Group " + group.getName()
								+ " failed: BRIGHT_MOON_DEF_CLOSE def_mld= "
								+ minimumLunarDistance + " ld= "
								+ lunarDistance);
						return false;
					}
				} else {
					// Group has specific MLD.
					if (lunarDistance < mld) {
						logger.log(3, "Group " + group.getName()
								+ " failed: BRIGHT_MOON_CLOSE spec_mld= " + mld
								+ " ld= " + lunarDistance);
						return false;
					}
				}

			}
			nobs++;
		}

		// VETO: No obs in group.
		if (nobs == 0) {
			logger.log(3, "Group " + group.getName()
					+ " failed: NO_OBS_IN_GROUP");
			return false;
		}

		return true;
	}

	// check if [t1,t2] overlaps with any RTI window from the list.*/
	private boolean overlapsUnavailableWindow(long t1, long t2) {
		TimeWindow testWindow = new TimeWindow(t1, t2);

		// check there are any windows
		if (unavailableWindows == null)
			return false;

		Iterator iw = unavailableWindows.iterator();
		while (iw.hasNext()) {
			TimeWindow window = (TimeWindow) iw.next();

			// If this window overlaps the test window then we have an overlap
			if (!testWindow.intersect(window).equals(TimeWindow.EMPTY_WINDOW)) {
				logger.log(3, "RTI_WINDOW_OVERLAP Test:" + testWindow
						+ " with " + window);
				return true;
			}
		}
		// ok no overlaps
		return false;

	}

	/**
	 * Returns true if there are any (executable) fixed groups after time.
	 * 
	 * @param time
	 *            The time after which we wish to find fixed groups.
	 */
	private boolean hasMoreFixedGroups(long time) {
		long nextTime = -1L;

		Iterator ifg = fixedGroups.iterator();
		while (ifg.hasNext()) {
			FixedGroup fg = (FixedGroup) ifg.next();
			long fgtime = fg.getFixedTime();
			// if we havnt already past its latest start time count it
			if (fgtime + fixedGroupPostStartLapseTime > time) {
				if (fgtime > nextTime) {
					nextTime = fgtime;
				}
			}
		}

		if (nextTime > 0)
			return true;
		return false;
	}

	/**
	 * Returns the time of the next (executable) fixed group after time.
	 * 
	 * @param time
	 *            The time after which we wish to find fixed groups.
	 */
	private FixedGroup getNextFixedGroup(long time) {
		long nextTime = -1L;
		FixedGroup nfg = null;

		Iterator ifg = fixedGroups.iterator();
		while (ifg.hasNext()) {
			FixedGroup fg = (FixedGroup) ifg.next();
			long fgtime = fg.getFixedTime();
			// if we havnt already past its latest start time count it
			if (fgtime + fixedGroupPostStartLapseTime > time) {
				if (fgtime > nextTime) {
					nextTime = fgtime;
					nfg = fg;
				}
			}
		}

		return nfg;
	}

	/** Returns true if group is tabu at time. */
	private boolean isTabu(Group group, long time) {
		if (!tabuGroups.containsKey(group))
			return false;
		Long lt = (Long) tabuGroups.get(group);
		long tt = lt.longValue();
		if (time < tt)
			return true;
		return false;
	}

}
